<!DOCTYPE html>
<html>
<head>
<title>InputEvent: beforeinput for Drag and Drop</title>
<script src="../../../resources/testharness.js"></script>
<script src="../../../resources/testharnessreport.js"></script>
<style>
div {
    width: 100px;
    height: 100px;
}
</style>
</head>
<body>
<div id="editable1" contenteditable><b id="boldtext">EditableText</b></div>
<div id="editable2" contenteditable></div>
<textarea id="textarea1">Text</textarea>
<textarea id="barrier"></textarea>
<input id="input_drag">
<input id="input_drop">
<script>
function simulateDragDrop(dragElement, dropElement, start, end) {
    if (dragElement.select) {
        if (start === undefined || end === undefined) {
            dragElement.select();
        } else {
            dragElement.focus();
            dragElement.setSelectionRange(start, end);
        }
    } else {
        var selection = window.getSelection();
        selection.collapse(dragElement, 0);
        selection.extend(dragElement, 1);
    }

    eventSender.mouseMoveTo(dragElement.offsetLeft + dragElement.offsetWidth / 2,
                            dragElement.offsetTop + dragElement.offsetHeight / 2);
    eventSender.mouseDown();
    eventSender.leapForward(600);
    eventSender.mouseMoveTo(dropElement.offsetLeft + dropElement.offsetWidth / 2,
                            dropElement.offsetTop + dropElement.offsetHeight / 2);
    eventSender.mouseUp();
}

function assertCleanInitialDOM(logInfo) {
    const editable1 = document.getElementById('editable1');
    const editable2 = document.getElementById('editable2');
    const textarea1 = document.getElementById('textarea1');
    assert_equals(editable1.children.length, 1, `${logInfo}, DOM is dirty`);
    assert_equals(editable1.children[0].innerHTML, 'EditableText', `${logInfo}, DOM is dirty`);
    assert_equals(editable2.children.length, 0, `${logInfo}, DOM is dirty`);
    assert_equals(textarea1.value, 'Text', `${logInfo}, DOM is dirty`);
}

test(function() {
    assertCleanInitialDOM();
    assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.');
    assert_not_equals(window.testRunner, undefined, 'This test requires testRunner.');

    const editable1 = document.getElementById('editable1');
    const editable2 = document.getElementById('editable2');
    const textarea1 = document.getElementById('textarea1');

    function preventDeleteByDragListener(event) {
        if (event.inputType == 'deleteByDrag')
            event.preventDefault();
    }

    function preventInsertFromDropListener(event) {
        if (event.inputType == 'insertFromDrop')
            event.preventDefault();
    }

    const undoBarrier = document.getElementById('barrier');
    undoBarrier.focus();
    document.execCommand('insertText', false, 'abc');
    function assertBarrierUnchanged(log) {
        assert_equals(undoBarrier.value, 'abc', log);
    }

    // Normally Drag&Drop requires a single Undo.
    simulateDragDrop(editable1, editable2);
    assert_equals(editable1.children.length, 0, `Normal Drag&Drop should remove data from editable1.`);
    assert_equals(editable2.children.length, 1, `Normal Drag&Drop should insert data into editable2.`);
    testRunner.execCommand('undo');
    assertCleanInitialDOM('Normal Drag&Drop');
    assertBarrierUnchanged('step 1');

    // Canceling |DeleteByDrag|, still require a single Undo.
    editable1.addEventListener('beforeinput', preventDeleteByDragListener);
    simulateDragDrop(editable1, editable2);
    testRunner.execCommand('undo');
    assertCleanInitialDOM('Canceling |DeleteByDrag|');
    editable1.removeEventListener('beforeinput', preventDeleteByDragListener);
    assertBarrierUnchanged('step 2');

    // Canceling |InsertFromDrop|, still require a single Undo.
    editable2.addEventListener('beforeinput', preventInsertFromDropListener);
    simulateDragDrop(editable1, editable2);
    testRunner.execCommand('undo');
    assertCleanInitialDOM('Canceling |InsertFromDrop|');
    editable2.removeEventListener('beforeinput', preventInsertFromDropListener);
    assertBarrierUnchanged('step 3');

    // Canceling both, shouldn't create undo entry.
    editable1.addEventListener('beforeinput', preventDeleteByDragListener);
    editable2.addEventListener('beforeinput', preventInsertFromDropListener);
    simulateDragDrop(editable1, editable2);
    assertCleanInitialDOM('Canceling both');
    testRunner.execCommand('undo');
    assert_equals(undoBarrier.value, '');
    testRunner.execCommand('redo');
    assertBarrierUnchanged('step 4');
    editable1.removeEventListener('beforeinput', preventDeleteByDragListener);
    editable2.removeEventListener('beforeinput', preventInsertFromDropListener);

    // Two Drag&Drop, cancel first |InsertFromDrop| and second |DeleteByDrag|, should still create 2 undo entries.
    editable2.addEventListener('beforeinput', preventInsertFromDropListener);
    simulateDragDrop(editable1, editable2);
    editable2.removeEventListener('beforeinput', preventInsertFromDropListener);
    textarea1.addEventListener('beforeinput', preventDeleteByDragListener);
    textarea1.select();
    simulateDragDrop(textarea1, editable2);
    textarea1.removeEventListener('beforeinput', preventDeleteByDragListener);
    assert_equals(editable1.children.length, 0);
    assert_equals(editable2.innerHTML, 'Text');
    assert_equals(textarea1.value, 'Text');
    // First undo.
    testRunner.execCommand('undo');
    assert_equals(editable1.children.length, 0);
    assert_equals(editable2.innerHTML, '');
    assert_equals(textarea1.value, 'Text');
    // Second undo.
    testRunner.execCommand('undo');
    assert_equals(editable1.children.length, 1);
    assert_equals(editable2.innerHTML, '');
    assert_equals(textarea1.value, 'Text');
    // More undo should reach to |undoBarrier|.
    assertBarrierUnchanged('step 5');
    testRunner.execCommand('undo');
    assert_equals(undoBarrier.value, '');
    testRunner.execCommand('redo');
}, 'Testing Drag and Drop, preventDefault() and Undo entry');

test(function() {
    assertCleanInitialDOM();
    assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.');
    assert_not_equals(window.testRunner, undefined, 'This test requires testRunner.');

    const editable1 = document.getElementById('editable1');
    const editable2 = document.getElementById('editable2');
    var eventOrderRecorder = [];
    document.addEventListener('beforeinput', event =>
        eventOrderRecorder.push(`beforeinput:${event.target.id}:${event.inputType}`));
    document.addEventListener('input', event =>
        eventOrderRecorder.push(`input:${event.target.id}:${event.inputType}`));
    ['drop', 'dragend'].forEach(eventType => document.addEventListener(
            eventType, () => eventOrderRecorder.push(`${event.target.id}:${eventType}`)));

    function testDragDropEventOrder(dragElement, dropElement, expectedOrder) {
        assert_equals(dragElement.children.length, 1);
        eventOrderRecorder = [];
        simulateDragDrop(dragElement, dropElement);
        assert_array_equals(eventOrderRecorder, expectedOrder,
            `Testing drag ${dragElement.id} onto ${dropElement.id} actual order: ${eventOrderRecorder}`);
    }

    // Test Drag and Drop.
    testDragDropEventOrder(editable1, editable2,
        ['editable2:drop', 'beforeinput:boldtext:deleteByDrag', 'input:editable1:deleteByDrag',
        'beforeinput:editable2:insertFromDrop', 'input:editable2:insertFromDrop', 'editable1:dragend']);
    testRunner.execCommand('undo');
}, 'Testing Drag and Drop event order');

test(function() {
    assertCleanInitialDOM();
    assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.');
    assert_not_equals(window.testRunner, undefined, 'This test requires testRunner.');

    const editable1 = document.getElementById('editable1');
    const editable2 = document.getElementById('editable2');
    var lastPlainTextData = {};
    var lastHTMLData = {};
    document.addEventListener('beforeinput', event => {
        lastPlainTextData[event.inputType] = event.dataTransfer ? event.dataTransfer.getData('text/plain') : null;
        lastHTMLData[event.inputType] = event.dataTransfer ? event.dataTransfer.getData('text/html') : null;
    });

    function testDragDropDataTransfer(inputType, dragElement, dropElement, expectedPlainText, expectedHTML) {
        assert_equals(dragElement.children.length, 1);
        lastPlainTextData = {};
        lastHTMLData = {};
        simulateDragDrop(dragElement, dropElement);
        assert_equals(lastPlainTextData[inputType], expectedPlainText,
            `Testing '${inputType}' getData('text/plain')`);
        if (expectedHTML && expectedHTML.test) {
            assert_regexp_match(lastHTMLData[inputType], expectedHTML,
                `Testing '${inputType}' getData('text/html')`);
        } else {
            assert_equals(lastHTMLData[inputType], expectedHTML,
                `Testing '${inputType}' getData('text/html')`);
        }
    }

    // Test Drag and Drop.
    testDragDropDataTransfer('deleteByDrag', editable1, editable2, null, null);
    testRunner.execCommand('undo');
    testDragDropDataTransfer('insertFromDrop', editable1, editable2, 'EditableText', /^.*EditableText<\/b>$/);
    testRunner.execCommand('undo');
}, 'Testing Drag and Drop dataTransfer');

test(function() {
    assertCleanInitialDOM();
    assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.');
    assert_not_equals(window.testRunner, undefined, 'This test requires testRunner.');

    const editable1 = document.getElementById('editable1');
    const editable2 = document.getElementById('editable2');
    var inputTypesToPrevent = [];
    document.addEventListener('beforeinput', event => {
        if (inputTypesToPrevent.indexOf(event.inputType) != -1)
            event.preventDefault();
    });

    function testDragDropPreventDefault(preventDefaultTypes, dragElement, dropElement, expectedDragElementChildren, expectedDropElementChildren) {
        assert_equals(dragElement.children.length, 1);
        inputTypesToPrevent = preventDefaultTypes;
        simulateDragDrop(dragElement, dropElement);
        assert_equals(dragElement.children.length, expectedDragElementChildren,
            'Testing preventDefault() on ${preventDefaultTypes} ${dragElement.id} children count');
        assert_equals(dropElement.children.length, expectedDropElementChildren,
            'Testing preventDefault() on ${preventDefaultTypes} ${dropElement.id} children count');
        inputTypesToPrevent = [];
    }

    // Preventing single 'beforeinput' will only cancel DOM update for one event,
    // the remaining DOM update will still update undo stack.
    testDragDropPreventDefault(['deleteByDrag'], editable1, editable2, 1, 1);
    testRunner.execCommand('undo');
    testDragDropPreventDefault(['insertFromDrop'], editable1, editable2, 0, 0);
    testRunner.execCommand('undo');

    // Adding 'insertHTML' command to undo stack.
    editable2.focus();
    document.execCommand('insertHTML', false, '<b>B</b><i>i</i>');
    assert_equals(editable2.children.length, 2,
        '"editable2" should have 2 children after "insertHTML" command');
    // Canceling both |deleteByDrag| and |insertFromDrop| won't modify undo stack.
    testDragDropPreventDefault(['deleteByDrag', 'insertFromDrop'], editable1, editable2, 1, 2);
    // |undo| will undo last 'insertHTML' command.
    testRunner.execCommand('undo');
    assert_equals(editable2.children.length, 0,
        '"editable2" should have 0 children after undo "insertHTML"');
}, 'Testing Drag and Drop preventDefault()');

test(function() {
    assertCleanInitialDOM();
    assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.');
    assert_not_equals(window.testRunner, undefined, 'This test requires testRunner.');

    const editable1 = document.getElementById('editable1');
    const editable2 = document.getElementById('editable2');
    var eventOrderRecorder = [];
    [editable1, editable2].forEach(editable => {
        editable.addEventListener('beforeinput', event =>
            eventOrderRecorder.push(`beforeinput:${editable.id}:${event.inputType}`));
        editable.addEventListener('input', event =>
            eventOrderRecorder.push(`input:${editable.id}:${event.inputType}`));
        editable.addEventListener('drop', event =>
            eventOrderRecorder.push(`${editable.id}:drop`));
        editable.addEventListener('dragend', event =>
            eventOrderRecorder.push(`${editable.id}:dragend`));
    });

    function testDragDropEventOrder(dragElement, dropElement, expectedOrder) {
        assert_equals(dragElement.children.length, 1);
        eventOrderRecorder = [];
        simulateDragDrop(dragElement, dropElement);
        assert_array_equals(eventOrderRecorder, expectedOrder,
            `Testing drag ${dragElement.id} onto ${dropElement.id} actual order: ${eventOrderRecorder}`);
    }

    function removeEditable1Listener() {
        editable1.remove();
    }

    function removeEditable2Listener() {
        editable2.remove();
    }

    // Testing remove drop target, |editable2| won't get 'beforeinput' as it's disconnected.
    editable1.addEventListener('beforeinput', removeEditable2Listener);
    testDragDropEventOrder(editable1, editable2,
        ['editable2:drop', 'beforeinput:editable1:deleteByDrag', 'input:editable1:deleteByDrag', 'editable1:dragend']);
    editable1.removeEventListener('beforeinput', removeEditable2Listener);
    testRunner.execCommand('undo');
    document.body.appendChild(editable2);

    // Testing remove drag target, |editable1| won't receive DOM updates after disconnected.
    editable1.addEventListener('beforeinput', removeEditable1Listener);
    testDragDropEventOrder(editable1, editable2,
        ['editable2:drop', 'beforeinput:editable1:deleteByDrag', 'beforeinput:editable2:insertFromDrop',
        'input:editable2:insertFromDrop', 'editable1:dragend']);
    editable1.removeEventListener('beforeinput', removeEditable1Listener);
    testRunner.execCommand('undo');
    document.body.appendChild(editable1);
}, 'Testing element removed by event handler');

test(() => {
    assert_not_equals(window.eventSender, undefined, 'This test requires eventSender.');
    assert_not_equals(window.testRunner, undefined, 'This test requires testRunner.');

    const input_drag = document.getElementById('input_drag');
    const input_drop = document.getElementById('input_drop');

    input_drag.value = '12345678';
    input_drop.value = '4';

    simulateDragDrop(input_drag, input_drop, 1, 8);

    assert_equals(input_drag.value, '1');
    assert_equals(input_drop.value, '42345678');

    testRunner.execCommand('undo');

    assert_equals(input_drag.value, '12345678');
    assert_equals(input_drop.value, '4');
}, 'Undo drag&drop should update input.value');
</script>
</body>
</html>
